package com.gitee.wsl.transform.format.number

/*
 * Copyright (c) 2018-2021. data2viz sàrl.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 */

//package io.data2viz.format

import com.gitee.wsl.ext.number.toPrettyNumberString
import com.gitee.wsl.transform.format.DoubleFormat
import com.gitee.wsl.transform.format.Format
import kotlin.math.*
import kotlin.math.abs
import kotlin.math.roundToLong

private val prefixes = listOf("y", "z", "a", "f", "p", "n", "µ", "m", "", "k", "M", "G", "T", "P", "E", "Z", "Y")
private var prefixExponent = 0

data class CoefficientExponent(val coefficient: String, val exponent: Int)

//fun Format.formatter(specify: String): DoubleFormat = formatter(specify)

fun Format.Companion.doubleFormatter(specify: String, locale: Locale = Locale()): DoubleFormat = locale.doubleFormatter(specify)

fun Locale.doubleFormatter(specifier: String): DoubleFormat = doubleFormatter(specify(specifier))

fun Format.Companion.doubleFormatter(
    type: Type = Type.NONE,
    fill: Char = ' ',
    align: Align = Align.RIGHT,
    sign: Sign = Sign.MINUS,
    symbol: PrefixSymbol? = null,
    zero: Boolean = false,
    width: Int? = null,
    group: Boolean = false,
    precision: Int? = null
): DoubleFormat = Locale().doubleFormatter(specify(type, fill, align, sign, symbol, zero, width, group, precision))

 fun Locale.doubleFormatter(
     type: Type = Type.NONE,
     fill: Char = ' ',
     align: Align = Align.RIGHT,
     sign: Sign = Sign.MINUS,
     symbol: PrefixSymbol? = null,
     zero: Boolean = false,
     width: Int? = null,
     groupSeparation: Boolean = false,
     precision: Int? = null
): DoubleFormat = doubleFormatter(specify(type, fill, align, sign, symbol, zero, width, groupSeparation, precision))

private fun Locale.doubleFormatter(spec: FormatSpec): DoubleFormat {

    val width = spec.width

    val groupFunction = formatGroup(grouping, groupSeparator)

    // Compute the prefix and suffix.
    // For SI-prefix, the suffix is lazily computed.
    val prefix = if (spec.symbol == PrefixSymbol.CURRENCY)
        currency[0]
    else if (spec.symbol == PrefixSymbol.NUMBER_BASE && spec.type.isNumberBase) "0" + spec.type.c.lowercase() else ""

    val suffix = if (spec.symbol == PrefixSymbol.CURRENCY) currency[1] else if (spec.type.isPercent) "%" else ""

    // What format function should we use?
    // Is this an integer type?
    // Can this type generate exponential notation?
    val formatType = formatTypes(spec.type)
    val maybeSuffix = spec.type.maybeSuffix

    // Set the default exponent if not specified,
    // or clamp the specified exponent to the supported range.
    // For significant exponent, it must be in [1, 21].
    // For fixed exponent, it must be in [0, 20].
    val precision = if (spec.precision == null) {
        if (spec.type != Type.NONE) 6 else 12
    } else {
        if (gprs.contains(spec.type))
            spec.precision.coerceIn(1, 21) else spec.precision.coerceIn(0, 20)
    }

    @Suppress("DEPRECATION")
    fun _format(value: Double): String {

        var returnValue: String

        var valuePrefix = prefix
        var valueSuffix = suffix

//        if (spec.type == Type.CHAR) {
//            valueSuffix = formatType(value, 0) + valueSuffix
//            returnValue = ""
//        } else {
        // value = +value              // typé

        // Perform the initial formatting.
        var valueNegative = value < 0
        returnValue = formatType(abs(value), precision)

        // If a negative value rounds to zero during formatting, treat as positive.
        if (valueNegative && returnValue.toDouble() == 0.0) valueNegative = false

        // Compute the prefix and suffix.
        valuePrefix = (if (valueNegative) (if (spec.sign == Sign.PARENTHESES) spec.sign.c else "-") else if (spec.sign == Sign.MINUS || spec.sign == Sign.PARENTHESES) "" else spec.sign.c) + valuePrefix
        valueSuffix = valueSuffix + (if (spec.type == Type.DECIMAL_WITH_SI) prefixes[8 + prefixExponent / 3] else "") + (if (valueNegative && spec.sign == Sign.PARENTHESES) ")" else "")

        // Break the formatted value into the integer “value” part that can be
        // grouped, and fractional or exponential “suffix” part that is not.
        if (maybeSuffix) {
            var i = -1
            val n = returnValue.length
            while (++i < n) {
                val c = returnValue[i]
                if (c !in '0'..'9') {
                    valueSuffix = (if (c == '.') decimalSeparator + returnValue.slice(i + 1 until returnValue.length) else returnValue.slice(i until returnValue.length)) + valueSuffix
                    returnValue = returnValue.slice(0 until i)
                    break
                }
            }
        }
//        }

        // If the fill character is not "0", grouping is applied before padding.
        if (spec.groupSeparation && !spec.zero) returnValue = groupFunction(returnValue, 9999999)

        // Compute the padding.
        val length = valuePrefix.length + returnValue.length + valueSuffix.length
        var padding = if (width != null && length < width) "".padStart(width - length, spec.fill) else ""

        // If the fill character is "0", grouping is applied after padding.
        if (spec.groupSeparation && spec.zero) {
            returnValue = groupFunction(padding + returnValue, if (padding.isNotEmpty()) width!! - valueSuffix.length else 9999999)
            padding = ""
        }

        // Reconstruct the final output based on the desired alignment.
        returnValue = when (spec.align) {
            Align.LEFT -> valuePrefix + returnValue + valueSuffix + padding
            Align.RIGHT_WITHOUT_SIGN -> valuePrefix + padding + returnValue + valueSuffix
            Align.CENTER -> {
                val padLength = padding.length / 2 - 1
                padding.slice(0..padLength) + valuePrefix + returnValue + valueSuffix + padding.slice(0 until padding.length - 1 - padLength)
            }
             Align.RIGHT -> padding + valuePrefix + returnValue + valueSuffix
        }
        return numerals(returnValue)
    }

    return DoubleFormat{_format(it)}
}

 fun Locale.numerals(valueAsString: String): String =
    if (numerals == null)
        valueAsString
    else valueAsString
        .fold(StringBuilder()) { acc, c ->
            val intValue = c.code
            if (intValue in 48..57)
                acc.append(numerals!![intValue - 48])
            else
                acc.append(c)
        }
        .toString()


 fun formatPrefix(specifier: String, fixedPrefix: Double): DoubleFormat = Locale().formatPrefix(specifier, fixedPrefix)


/**
 * Equivalent to locale.format, except the returned function will convert values to the units of
 * the appropriate [SI prefix][https://en.wikipedia.org/wiki/Metric_prefix#List_of_SI_prefixes]
 * for the specified numeric reference value before formatting in fixed point notation.
 *
 * The following prefixes are supported:
 * - y - yocto, 10⁻²⁴
 * - z - zepto, 10⁻²¹
 * - a - atto, 10⁻¹⁸
 * - f - femto, 10⁻¹⁵
 * - p - pico, 10⁻¹²
 * - n - nano, 10⁻⁹
 * - µ - micro, 10⁻⁶
 * - m - milli, 10⁻³
 * - (none) - 10⁰
 * - k - kilo, 10³
 * - M - mega, 10⁶
 * - G - giga, 10⁹
 * - T - tera, 10¹²
 * - P - peta, 10¹⁵
 * - E - exa, 10¹⁸
 * - Z - zetta, 10²¹
 * - Y - yotta, 10²⁴
 */
 fun Locale.formatPrefix(specifier: String, fixedPrefix: Double): DoubleFormat {
    val formatSpecifier = specify(specifier).copy(type = Type.FIXED_POINT)
    val f = doubleFormatter(formatSpecifier)
    val e = floor(exponent(fixedPrefix).toDouble() / 3.0).coerceIn(-8.0, 8.0) * 3
    val k = 10.0.pow(-e)
    val prefix = prefixes[8 + (e / 3.0).toInt()]
    return DoubleFormat{value-> f(k * value) + prefix}
}

private fun exponent(value: Double): Int {
    val x = formatDecimal(abs(value))
    return x?.exponent ?: 0
}

private fun formatGroup(group: List<Int>, groupSeparator: String): (String, Int) -> String {
    return fun(value: String, width: Int): String {
        var i = value.length
        val t = mutableListOf<String>()
        var j = 0
        var g = group[0]
        var length = 0

        while (i > 0 && g > 0) {
            if (length + g + 1 > width) g = max(1, width - length)
            i -= g
            t.add(value.substring(max(0, i), min(value.length, i + g)))
            length += g + 1
            if (length > width) break
            j = (j + 1) % group.size
            g = group[j]
        }

        t.reverse()
        return t.joinToString(separator = groupSeparator)
    }
}


private fun formatTypes(type: Type): (Double, Int) -> String =
    when (type) {
        Type.FIXED_POINT            -> { x: Double, p: Int -> x.toFixed(p) }
        Type.PERCENT                -> { x: Double, p: Int -> (x * 100).toFixed(p) }
        Type.PERCENT_ROUNDED        -> { x: Double, p: Int -> formatRounded(x * 100, p) }
//            Type.CHAR                   -> { x: Double, _: Int -> "$x" }
        Type.DECIMAL_ROUNDED        -> { x: Double, _: Int -> x.toStringDigits(10) }
        Type.DECIMAL                -> { x: Double, p: Int -> formatRounded(x, p) }
        Type.DECIMAL_WITH_SI        -> { x: Double, p: Int -> formatPrefixAuto(x, p) }
        Type.DECIMAL_OR_EXPONENT    -> { x: Double, p: Int -> x.toPrecision(p) }
        Type.EXPONENT               -> { x: Double, p: Int -> x.toExponential(p) }
        Type.BINARY                 -> { x: Double, _: Int -> x.toStringDigits(2) }
        Type.OCTAL                  -> { x: Double, _: Int -> x.toStringDigits(8) }
        Type.HEX_UPPERCASE          -> { x: Double, _: Int -> x.toStringDigits(16).uppercase() }
        Type.HEX_LOWERCASE          -> { x: Double, _: Int -> x.toStringDigits(16) }
        Type.NONE                   -> { x: Double, p: Int -> formatDefault(x,p)}
    }

private fun formatDefault(x: Double, p: Int): String {
    val newX = x.toPrecision(p)
    var i0 = -1
    var i1 = 0

    loop@ for (i in 1 until newX.length) {
        val c = newX[i]
        when (c) {
            '.' -> {
                i0 = i
                i1 = i
            }
            '0' -> {
                if (i0 == 0) i0 = i
                i1 = i
            }
            'e' -> break@loop
            else -> if (i0 > 0) i0 = 0
        }
    }

    return if (i0 > 0) newX.slice(0 until i0) + newX.slice(i1 + 1 until newX.length) else newX
}

 fun formatRounded(x: Double, p: Int): String {
    val ce = formatDecimal(x, p) ?: return "$x"

    return if (ce.exponent < 0) {
        "0.".padEnd(-ce.exponent + 1, '0') + ce.coefficient
    } else {
        if (ce.coefficient.length > ce.exponent + 1) {
            ce.coefficient.slice(0..ce.exponent) + "." +
                    ce.coefficient.slice(ce.exponent + 1 until ce.coefficient.length)
        } else {
            ce.coefficient + "".padStart(ce.exponent - ce.coefficient.length + 1, '0')
        }
    }
}

internal fun format(number: Float, digitsAfterComma: Int = 1): String =
    number.toString()
        .split('.')
        .toMutableList()
        .apply { this[1] = this[1].take(digitsAfterComma) }
        .joinToString(separator = ".")

/**
 * Computes the decimal coefficient and exponent of the specified number x with
 * significant digits p, where x is positive and p is in [1, 21] or undefined.
 * For example, formatDecimal(1.23) returns ["123", 0].
 */
 fun formatDecimal(x: Double, p: Int = 0): CoefficientExponent? {
    var newX: String = x.toExponential()
    if (p != 0) {
        newX = x.toExponential(p - 1)
    }

    val index = newX.indexOf("e")
    if (index < 0) return null

    val coefficient = newX.slice(0 until index)

    // The string returned by toExponential either has the form \d\.\d+e[-+]\d+
    // (e.g., 1.2e+3) or the form \de[-+]\d+ (e.g., 1e+3).
    val decimal: String = if (coefficient.length > 1) coefficient[0] + coefficient.slice(2 until coefficient.length) else coefficient

    val parsePrecision = newX.slice(index + 1 until newX.length).toIntOrNull()

    return CoefficientExponent(decimal, parsePrecision ?: 0)
}

private fun formatPrefixAuto(x: Double, p: Int = 0): String {
    val ce = formatDecimal(x, p) ?: return "$x"

    prefixExponent = floor(ce.exponent / 3.0).toInt().coerceIn(-8, 8) * 3
    val i = ce.exponent - prefixExponent + 1
    val n = ce.coefficient.length

    return when {
        i == n -> ce.coefficient
        i > n -> ce.coefficient.padEnd(i, '0')
        i > 0 -> ce.coefficient.slice(0 until i) + "." + ce.coefficient.slice(i until ce.coefficient.length)
        else -> "0.".padEnd(2 - i, '0') + formatDecimal(x, max(0, p + i - 1))!!.coefficient
    } // less than 1y!
}

 fun precisionFixed(step: Double): Int = max(0, -exponent(abs(step)))

 fun precisionPrefix(step: Double, value: Double): Int = (floor(exponent(value) / 3.0).toInt().coerceIn(-8, 8) * 3 - exponent(abs(step))).coerceAtLeast(0)

 fun precisionRound(step: Double, max: Double): Int {
    val newStep = abs(step)
    val newMax = abs(max) - step
    return max(0, exponent(newMax) - exponent(newStep)) + 1
}


internal fun Double.toStringDigits(radix: Int): String =
    if (this > Long.MAX_VALUE || this < Long.MIN_VALUE)
        toExponential()
    else
        (this).roundToLong().toString(radix)


/*internal  fun Double.toFixed(digits: Int): String =
    BigDecimal.fromDouble(this, DecimalMode(digits.toLong()))
        //.setScale(digits, RoundingMode.HALF_UP)
        .toString()*/

internal  fun Double.toFixed(digits: Int) = this.toPrettyNumberString(digits)
    /*java.math.BigDecimal(this)
        .setScale(digits, java.math.RoundingMode.HALF_UP)
        .toString()*/

internal  fun Double.toPrecision(digits: Int): String ="%.${digits}g".replace("e+0", "e+").replace("e-0", "e-")
    /*String.format( "%." + digits + "g", this)
        .replace("e+0", "e+")
        .replace("e-0", "e-")*/

internal  fun Double.toExponential(digits: Int): String ="%.${digits}g".replace("e+0", "e+").replace("e-0", "e-")
    /*String.format( "%." + digits + "e", this)
        .replace("e+0", "e+")
        .replace("e-0", "e-")*/

internal  fun Double.toExponential(): String {
    return toExponential(2)
//    val preciseNum = BigDecimal.fromDouble(abs(this))
//    val fractionalPart = (preciseNum % BigDecimal.ONE).toString()
//    val integerPart = preciseNum.div(BigDecimal.ONE).toString()
//    var fracRemovedDecExpZeros = fractionalPart.substring(fractionalPart.indexOfFirst { it == '.' }+1, fractionalPart.length)
//    val exponentIndex = fracRemovedDecExpZeros.indexOfFirst { it == 'e' || it == 'E' }
//    if (exponentIndex >= 0) fracRemovedDecExpZeros = fracRemovedDecExpZeros.substring(0,  exponentIndex)
//    fracRemovedDecExpZeros = fracRemovedDecExpZeros.dropWhile { it == '0' }
//    val fractionalPartSize = fracRemovedDecExpZeros.length
//    val commaPosition = integerPart.indexOfFirst { it == '.' }
//    val integerPartSize = if (commaPosition >= 0) integerPart.substring(0, commaPosition).length else integerPart.length
//
//    return "%$integerPartSize.${integerPartSize + fractionalPartSize - 1}e".replace("e+0", "e+")
//        .replace("e-0", "e-")
}

// TODO keep this, it will avoid errors when asking for high precision numbers (see FormatTests@check_platform_dependent_formatters_toFixed)
/*private fun Double.toFixedJVM(digits: Int): String {
    return BigDecimal(this.toString()).setScale(digits, BigDecimal.ROUND_HALF_UP).toString()
}*/
